﻿namespace MsForgeReboot.Web.Controllers {
    #region Usings

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Transactions;
    using System.Web.Mvc;
    using System.Web.Security;

    using Microsoft.Web.WebPages.OAuth;

    using MsForgeReboot.Framework.Entities;
    using MsForgeReboot.Framework.Interfaces;
    using MsForgeReboot.Services;
    using MsForgeReboot.Web.Extensions;
    using MsForgeReboot.Web.Models;

    using WebMatrix.WebData;

    #endregion

    [Authorize]
    public partial class AccountController : Controller {
        #region Fields

        private readonly IUserService userService;

        #endregion

        #region Constructors and Destructors

        public AccountController(IUserService svc) {
            this.userService = svc;
        }

        #endregion

        #region Enums

        public enum ManageMessageId {
            ChangePasswordSuccess,

            SetPasswordSuccess,

            RemoveLoginSuccess,

            ChangeProfileDetails,

            ManageSuccess
        }

        #endregion

        #region Public Methods and Operators

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Disassociate(string provider, string providerUserId) {
            var ownerAccount = OAuthWebSecurity.GetUserName(provider, providerUserId);
            ManageMessageId? message = null;

            // Only disassociate the account if the currently logged in user is the owner
            if (ownerAccount == this.User.Identity.Name) {
                // Use a transaction to prevent the user from deleting their last login credential
                using (
                    var scope = new TransactionScope(
                        TransactionScopeOption.Required,
                        new TransactionOptions { IsolationLevel = IsolationLevel.Serializable })) {
                    var hasLocalAccount =
                        OAuthWebSecurity.HasLocalAccount(WebSecurity.GetUserId(this.User.Identity.Name));
                    if (hasLocalAccount || OAuthWebSecurity.GetAccountsFromUserName(this.User.Identity.Name).Count > 1) {
                        OAuthWebSecurity.DeleteAccount(provider, providerUserId);
                        scope.Complete();
                        message = ManageMessageId.RemoveLoginSuccess;
                    }
                }
            }

            return this.RedirectToAction("Manage", new { Message = message });
        }

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public ActionResult ExternalLogin(string provider, string returnUrl) {
            return new ExternalLoginResult(
                provider, this.Url.Action("ExternalLoginCallback", new { ReturnUrl = returnUrl }));
        }

        [AllowAnonymous]
        public ActionResult ExternalLoginCallback(string returnUrl) {
            var result =
                OAuthWebSecurity.VerifyAuthentication(
                    this.Url.Action("ExternalLoginCallback", new { ReturnUrl = returnUrl }));
            if (!result.IsSuccessful) {
                return this.RedirectToAction("ExternalLoginFailure");
            }

            if (OAuthWebSecurity.Login(result.Provider, result.ProviderUserId, false)) {
                return this.RedirectToLocal(returnUrl);
            }

            if (this.User.Identity.IsAuthenticated) {
                // If the current user is logged in add the new account
                OAuthWebSecurity.CreateOrUpdateAccount(result.Provider, result.ProviderUserId, this.User.Identity.Name);
                return this.RedirectToLocal(returnUrl);
            }
            else {
                // Participants is new, ask for their desired membership name
                var loginData = OAuthWebSecurity.SerializeProviderUserId(result.Provider, result.ProviderUserId);
                this.ViewBag.ProviderDisplayName = OAuthWebSecurity.GetOAuthClientData(result.Provider).DisplayName;
                this.ViewBag.ReturnUrl = returnUrl;
                return this.View(
                    "ExternalLoginConfirmation",
                    new RegisterExternalLoginModel {
                                                       UserName = result.UserName,
                                                       ExternalLoginData = loginData,
                                                       PublicName =
                                                           this.getNameForExternalAuth(result.Provider, result.ExtraData)
                                                   });
            }
        }

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public ActionResult ExternalLoginConfirmation(RegisterExternalLoginModel model, string returnUrl) {
            string provider = null;
            string providerUserId = null;

            if (this.User.Identity.IsAuthenticated
                || !OAuthWebSecurity.TryDeserializeProviderUserId(
                    model.ExternalLoginData, out provider, out providerUserId)) {
                return this.RedirectToAction("Manage");
            }

            if (this.ModelState.IsValid) {
             
                using (var db = new MsForgeContext()) {
                    var user = db.Users.FirstOrDefault(u => u.Username.ToLower() == model.UserName.ToLower());
                    // Check if user already exists
                    if (user == null) {
                        // Insert name into the profile table
                        db.Users.Add(
                            new User {
                                         Username = model.UserName,
                                         IsValidated = true,
                                         PublicName = model.PublicName,
                                         ReceiveNotifications = true
                                     });
                        db.SaveChanges();

                        OAuthWebSecurity.CreateOrUpdateAccount(provider, providerUserId, model.UserName);
                        Roles.AddUserToRole(model.UserName, "Visitors");
                        OAuthWebSecurity.Login(provider, providerUserId, createPersistentCookie: false);

                        return this.RedirectToLocal(returnUrl);
                    }
                    else {
                        this.ModelState.AddModelError(
                            "UserName", "Participants name already exists. Please enter a different user name.");
                    }
                }
            }

            this.ViewBag.ProviderDisplayName = OAuthWebSecurity.GetOAuthClientData(provider).DisplayName;
            this.ViewBag.ReturnUrl = returnUrl;
            return View(model);
        }

        //
        // GET: /Account/ExternalLoginFailure

        [AllowAnonymous]
        public ActionResult ExternalLoginFailure() {
            return this.View();
        }

        [AllowAnonymous]
        [ChildActionOnly]
        public ActionResult ExternalLoginsList(string returnUrl) {
            this.ViewBag.ReturnUrl = returnUrl;
            return this.PartialView("_ExternalLoginsListPartial", OAuthWebSecurity.RegisteredClientData);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult LogOff() {
            WebSecurity.Logout();

            return this.RedirectToAction("Index", "Home");
        }

        [AllowAnonymous]
        public ActionResult Login(string returnUrl) {
            this.ViewBag.ReturnUrl = returnUrl;
            return this.View();
        }

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public ActionResult Login(LoginModel model, string returnUrl) {
            if (this.ModelState.IsValid
                && WebSecurity.Login(model.UserName, model.Password, persistCookie: model.RememberMe)) {
                return this.RedirectToLocal(returnUrl);
            }

            var ol = new OldLogin();
            if (this.ModelState.IsValid && ol.Login(model.UserName, model.Password, persistCookie: model.RememberMe)) {
                return this.RedirectToLocal(returnUrl);
            }

            // If we got this far, something failed, redisplay form
            this.ModelState.AddModelError("", "The user name or password provided is incorrect.");
            return View(model);
        }

        public ActionResult Manage(ManageMessageId? message) {
            this.ViewBag.StatusMessage = message == ManageMessageId.ChangePasswordSuccess
                                             ? "Lozinka je uspešno izmenjena."
                                             : message == ManageMessageId.SetPasswordSuccess
                                                   ? "Uspešno ste postavili novu lozinku."
                                                   : message == ManageMessageId.RemoveLoginSuccess
                                                         ? "Eksterni korisnični nalog je uspešno uklonjen."
                                                         : message == ManageMessageId.ChangeProfileDetails
                                                               ? "Uspešno ste izmenili profil."
                                                               : message == ManageMessageId.ManageSuccess
                                                                     ? "Opšta izmena profila je uspešno urađena."
                                                                     : "";
            var userID = WebSecurity.GetUserId(this.User.Identity.Name);
            this.ViewBag.HasLocalPassword = OAuthWebSecurity.HasLocalAccount(userID);
            this.ViewBag.ReturnUrl = this.Url.Action("Manage");
            var u = this.userService.GetSingleUser(userID);
            var model = new LocalPasswordProfileModel {
                                                          UserName = u.Username,
                                                          Notifications = u.ReceiveNotifications,
                                                          PublicName = u.PublicName
                                                      };
            return View(model);
        }

        //
        // POST: /Account/Manage

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Manage(LocalPasswordProfileModel model) {
            var hasLocalAccount = OAuthWebSecurity.HasLocalAccount(WebSecurity.GetUserId(this.User.Identity.Name));
            this.ViewBag.HasLocalPassword = hasLocalAccount;
            this.ViewBag.ReturnUrl = this.Url.Action("Manage");
            var changePasswordSucceeded = false;
            var changeDetailsSucceeded = false;
            // We need to see if the user submitted the password or not
            if (!String.IsNullOrEmpty(model.OldPassword) || !String.IsNullOrEmpty(model.NewPassword)) {
                if (hasLocalAccount) {
                    if (this.ModelState.IsValid) {
                        // ChangePassword will throw an exception rather than return false in certain failure scenarios.
                        try {
                            changePasswordSucceeded = WebSecurity.ChangePassword(
                                this.User.Identity.Name, model.OldPassword, model.NewPassword);
                        }
                        catch (Exception) {
                            changePasswordSucceeded = false;
                        }
                    }
                    // TODO change the below to encompass the entire thing
                }
                else {
                    // Participants does not have a local password so remove any validation errors caused by a missing
                    // OldPassword field
                    var state = this.ModelState["OldPassword"];
                    if (state != null) {
                        state.Errors.Clear();
                    }

                    if (this.ModelState.IsValid) {
                        try {
                            WebSecurity.CreateAccount(this.User.Identity.Name, model.NewPassword);
                            return this.RedirectToAction("Manage", new { Message = ManageMessageId.SetPasswordSuccess });
                        }
                        catch (Exception e) {
                            this.ModelState.AddModelError("", e);
                        }
                    }
                }
            }

            try {
                var userID = WebSecurity.GetUserId(this.User.Identity.Name);
                this.userService.SaveOrEdit(userID, model.UserName, model.PublicName, model.Notifications);
                changeDetailsSucceeded = true;
            }
            catch (Exception) {
                changeDetailsSucceeded = false;
            }

            if (changePasswordSucceeded && changeDetailsSucceeded) {
                return this.RedirectToAction("Manage", new { Message = ManageMessageId.ManageSuccess });
            }
            else {
                this.ModelState.AddModelError("", "The current password is incorrect or the new password is invalid.");
            }

            // If we got this far, something failed, redisplay form
            return View(model);
        }

        [AllowAnonymous]
        public ActionResult Register() {
            return this.View();
        }

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public ActionResult Register(RegisterModel model) {
            if (this.ModelState.IsValid) {
                // Attempt to register the user
                try {
                    WebSecurity.CreateUserAndAccount(
                        model.UserName,
                        model.Password,
                        new { model.PublicName, ReceiveNotifications = model.Notifications, IsValidated = false });
                    Roles.AddUserToRole(model.UserName, "Visitors");
                    WebSecurity.Login(model.UserName, model.Password);
                    return this.RedirectToAction("Index", "Home");
                }
                catch (MembershipCreateUserException e) {
                    this.ModelState.AddModelError("", ErrorCodeToString(e.StatusCode));
                }
            }

            // If we got this far, something failed, redisplay form
            return View(model);
        }

        [ChildActionOnly]
        public ActionResult RemoveExternalLogins() {
            var accounts = OAuthWebSecurity.GetAccountsFromUserName(this.User.Identity.Name);
            var externalLogins = new List<ExternalLogin>();
            foreach (var account in accounts) {
                var clientData = OAuthWebSecurity.GetOAuthClientData(account.Provider);

                externalLogins.Add(
                    new ExternalLogin {
                                          Provider = account.Provider,
                                          ProviderDisplayName = clientData.DisplayName,
                                          ProviderUserId = account.ProviderUserId,
                                      });
            }

            this.ViewBag.ShowRemoveButton = externalLogins.Count > 1
                                            || OAuthWebSecurity.HasLocalAccount(
                                                WebSecurity.GetUserId(this.User.Identity.Name));
            return this.PartialView("_RemoveExternalLoginsPartial", externalLogins);
        }

        #endregion

        #region Methods

        private static string ErrorCodeToString(MembershipCreateStatus createStatus) {
            // See http://go.microsoft.com/fwlink/?LinkID=177550 for
            // a full list of status codes.
            switch (createStatus) {
                case MembershipCreateStatus.DuplicateUserName:
                    return "Participants name already exists. Please enter a different user name.";

                case MembershipCreateStatus.DuplicateEmail:
                    return
                        "A user name for that e-mail address already exists. Please enter a different e-mail address.";

                case MembershipCreateStatus.InvalidPassword:
                    return "The password provided is invalid. Please enter a valid password value.";

                case MembershipCreateStatus.InvalidEmail:
                    return "The e-mail address provided is invalid. Please check the value and try again.";

                case MembershipCreateStatus.InvalidAnswer:
                    return "The password retrieval answer provided is invalid. Please check the value and try again.";

                case MembershipCreateStatus.InvalidQuestion:
                    return "The password retrieval question provided is invalid. Please check the value and try again.";

                case MembershipCreateStatus.InvalidUserName:
                    return "The user name provided is invalid. Please check the value and try again.";

                case MembershipCreateStatus.ProviderError:
                    return
                        "The authentication provider returned an error. Please verify your entry and try again. If the problem persists, please contact your system administrator.";

                case MembershipCreateStatus.UserRejected:
                    return
                        "The user creation request has been canceled. Please verify your entry and try again. If the problem persists, please contact your system administrator.";

                default:
                    return
                        "An unknown error occurred. Please verify your entry and try again. If the problem persists, please contact your system administrator.";
            }
        }

        private ActionResult RedirectToLocal(string returnUrl) {
            if (this.Url.IsLocalUrl(returnUrl)) {
                return this.Redirect(returnUrl);
            }
            else {
                return this.RedirectToAction("Index", "Home");
            }
        }

        private string getNameForExternalAuth(string provider, IDictionary<string, string> data) {
            string result;
            switch (provider) {
                case "twitter":
                case "facebook":
                    result = data["name"];
                    break;
                case "google":
                    result = String.Format("{0} {1}", data["firstName"], data["lastName"]);
                    break;
                default:
                    throw new InvalidOperationException("Provider not recognized");
            }

            return result;
        }

        #endregion

        internal class ExternalLoginResult : ActionResult {
            #region Constructors and Destructors

            public ExternalLoginResult(string provider, string returnUrl) {
                this.Provider = provider;
                this.ReturnUrl = returnUrl;
            }

            #endregion

            #region Public Properties

            public string Provider { get; private set; }

            public string ReturnUrl { get; private set; }

            #endregion

            #region Public Methods and Operators

            public override void ExecuteResult(ControllerContext context) {
                OAuthWebSecurity.RequestAuthentication(this.Provider, this.ReturnUrl);
            }

            #endregion
        }
    }
}